package org.apache.lucene.ocean.util;

import java.io.Externalizable;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * Byte buffers maintained in a list. Can obtain OutputStream and InputStream
 * for writing and reading to buffers.
 * 
 */
// TODO: make Bytes externalizable
public class Bytes implements Externalizable {
  private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
  private List<byte[]> buffers = new ArrayList<byte[]>();
  private List<byte[]> bufferPool = new ArrayList<byte[]>();
  private int bufferSize;
  private int count = 0;
  private ByteArrayOutputStream output;

  public Bytes(int bufferSize) {
    this.bufferSize = bufferSize;
  }
  
  public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    int size = in.readInt();
    for (int x=0; x < size; x++) {
      int len = in.readInt();
      byte[] bytes = new byte[len];
      in.read(bytes);
      buffers.add(bytes);
    }
  }
  
  public void writeExternal(ObjectOutput out) throws IOException {
    out.writeInt(buffers.size());
    for (int x=0; x < buffers.size(); x++) {
      byte[] bytes = buffers.get(x);
      out.writeInt(bytes.length);
      out.write(bytes);
    }
  }
  
  public void writeTo(OutputStream out) throws IOException {
    int remaining = count;
    for (int i = 0; i < buffers.size(); i++) {
      byte[] buf = getBuffer(i);
      int c = Math.min(buf.length, remaining);
      out.write(buf, 0, c);
      remaining -= c;
      if (remaining == 0) {
        break;
      }
    }
  }

  public void reset() {
    count = 0;
    output = null;
    bufferPool.addAll(buffers);
    buffers.clear();
  }

  public int length() {
    return count;
  }

  private byte[] getBuffer(int index) {
    return (byte[]) buffers.get(index);
  }

  /**
   * public class ByteArrayInputStream extends InputStream { private int pos =
   * 0; private int bufferPos = 0; private byte[] buffer; private int bufferLen;
   * private int bufferIdx = 0;
   * 
   * public ByteArrayInputStream() { if (buffers.size() > 0) { buffer =
   * buffers.get(bufferIdx); } }
   * 
   * public int read(byte[] b, int offset, int len) throws IOException { if (pos >=
   * count) return -1; int total = 0; while (len > 0) { if (bufferPos >=
   * buffer.length) { if (!nextBuffer()) { return total; } } int remainInBuffer =
   * buffer.length - bufferPos; int bytesToCopy = len < remainInBuffer ? len :
   * remainInBuffer; System.arraycopy(buffer, bufferPos, b, offset,
   * bytesToCopy); offset += bytesToCopy; len -= bytesToCopy; bufferPos +=
   * bytesToCopy; total += bytesToCopy; pos += bytesToCopy; } return total; }
   * 
   * public int read() { if (buffers.size() == 0) return -1; if (pos >= count)
   * return -1; if (bufferPos >= buffer.length) { if (!nextBuffer()) return -1; }
   * pos++; return buffer[bufferPos++]; }
   * 
   * private boolean nextBuffer() { bufferIdx++; if (bufferIdx >=
   * buffers.size()-1) { return false; } buffer = buffers.get(bufferIdx); return
   * true; } }
   */

  public class ByteArrayInputStream extends InputStream {
    private byte[] currentBuffer;
    private int currentBufferIndex;
    private int bufferPosition;
    private long bufferStart;
    private int bufferLength;

    public ByteArrayInputStream() {
      currentBufferIndex = -1;
      currentBuffer = null;
    }

    public long getPosition() {
      return currentBufferIndex < 0 ? 0 : bufferStart + bufferPosition;
    }

    public int read() throws IOException {
      if (getPosition() >= count)
        return -1;
      if (bufferPosition >= bufferLength) {
        currentBufferIndex++;
        switchCurrentBuffer();
      }
      return currentBuffer[bufferPosition++];
    }

    public int read(byte[] b, int offset, int len) throws IOException {
      int pos = (int) getPosition();
      if (pos >= count)
        return -1;
      int total = 0;
      while (len > 0) {
        if (bufferPosition >= bufferLength) {
          currentBufferIndex++;
          if (!switchCurrentBuffer()) {
            return total;
          }
        }
        int remainInBuffer = bufferLength - bufferPosition;
        int bytesToCopy = len < remainInBuffer ? len : remainInBuffer;
        System.arraycopy(currentBuffer, bufferPosition, b, offset, bytesToCopy);
        offset += bytesToCopy;
        len -= bytesToCopy;
        bufferPosition += bytesToCopy;
        total += bytesToCopy;
      }
      return total;
    }

    private final boolean switchCurrentBuffer() throws IOException {
      if (currentBufferIndex >= buffers.size()) { // end of file reached, no
                                                  // more
        // buffers left
        return false;
      } else {
        currentBuffer = buffers.get(currentBufferIndex);
        bufferPosition = 0;
        bufferStart = (long) bufferSize * (long) currentBufferIndex;
        long buflen = count - bufferStart;
        bufferLength = buflen > bufferSize ? bufferSize : (int) buflen;
        return true;
      }
    }
  }

  public class ByteArrayOutputStream extends OutputStream {
    private byte[] currentBuffer;
    private int currentBufferIndex;

    private int bufferPosition;
    private long bufferStart;
    private int bufferLength;

    public ByteArrayOutputStream() throws IOException {
      seek(0);
    }

    public void seek(long pos) throws IOException {
      // set the file length in case we seek back
      // and flush() has not been called yet
      // setFileLength();
      if (pos < bufferStart || pos >= bufferStart + bufferLength) {
        currentBufferIndex = (int) (pos / bufferSize);
        switchCurrentBuffer();
      }
      bufferPosition = (int) (pos % bufferSize);
    }

    public void write(int b) throws IOException {
      if (bufferPosition == bufferLength) {
        currentBufferIndex++;
        switchCurrentBuffer();
      }
      currentBuffer[bufferPosition++] = (byte) b;
      count++;
    }

    public void write(byte[] b, int offset, int len) throws IOException {
      while (len > 0) {
        if (bufferPosition == bufferLength) {
          currentBufferIndex++;
          switchCurrentBuffer();
        }

        int remainInBuffer = currentBuffer.length - bufferPosition;
        int bytesToCopy = len < remainInBuffer ? len : remainInBuffer;
        System.arraycopy(b, offset, currentBuffer, bufferPosition, bytesToCopy);
        offset += bytesToCopy;
        len -= bytesToCopy;
        bufferPosition += bytesToCopy;
        count += bytesToCopy;
      }
    }

    private final void switchCurrentBuffer() throws IOException {
      if (currentBufferIndex == buffers.size()) {
        if (bufferPool.size() > 0) {
          currentBuffer = bufferPool.get(0);
          bufferPool.remove(0);
        } else {
          currentBuffer = new byte[bufferSize];
        }
        buffers.add(currentBuffer);
      } else {
        currentBuffer = (byte[]) buffers.get(currentBufferIndex);
      }
      bufferPosition = 0;
      bufferStart = (long) bufferSize * (long) currentBufferIndex;
      bufferLength = currentBuffer.length;
    }
  }

  /**
   * public class ByteArrayOutputStream extends OutputStream { private int
   * currentBufferIndex; private int pos = 0; private byte[] currentBuffer;
   * 
   * public ByteArrayOutputStream() { if (buffers.size() > 0) {
   * currentBufferIndex = 0; currentBuffer = getBuffer(currentBufferIndex); }
   * needNewBuffer(); }
   * 
   * private void needNewBuffer() { if (currentBufferIndex < buffers.size() - 1) { //
   * Recycling old buffer pos += currentBuffer.length;
   * 
   * currentBufferIndex++; currentBuffer = getBuffer(currentBufferIndex);
   * numValidBuffers++; } else { // Creating new buffer if (currentBuffer ==
   * null) { pos = 0; } else { pos += currentBuffer.length; }
   * 
   * currentBufferIndex++; currentBuffer = new byte[bufferSize];
   * buffers.add(currentBuffer); numValidBuffers++; } }
   * 
   * public void write(byte[] b, int off, int len) { if ((off < 0) || (off >
   * b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) {
   * throw new IndexOutOfBoundsException(); } else if (len == 0) { return; } int
   * newcount = count + len; int remaining = len; int inBufferPos = count - pos;
   * while (remaining > 0) { int part = Math.min(remaining, currentBuffer.length -
   * inBufferPos); System.arraycopy(b, off + len - remaining, currentBuffer,
   * inBufferPos, part); remaining -= part; if (remaining > 0) {
   * needNewBuffer(); inBufferPos = 0; } } count = newcount; }
   * 
   * public void write(int b) { int inBufferPos = count - pos; if (inBufferPos ==
   * currentBuffer.length) { needNewBuffer(); inBufferPos = 0; }
   * currentBuffer[inBufferPos] = (byte) b; count++; }
   * 
   * public int size() { return count; }
   * 
   * public void close() throws IOException { // nop }
   * 
   * public void reset() { count = 0; pos = 0; currentBufferIndex = 0;
   * currentBuffer = getBuffer(currentBufferIndex); }
   * 
   * public void writeTo(OutputStream out) throws IOException { int remaining =
   * count; for (int i = 0; i < buffers.size(); i++) { byte[] buf =
   * getBuffer(i); int c = Math.min(buf.length, remaining); out.write(buf, 0,
   * c); remaining -= c; if (remaining == 0) { break; } } }
   * 
   * /** Gets the curent contents of this byte stream as a byte array. The
   * result is independent of this stream.
   * 
   * @return the current contents of this output stream, as a byte array
   * @see java.io.ByteArrayOutputStream#toByteArray()
   * 
   * public byte[] toByteArray() { int remaining = count; if (remaining == 0) {
   * return EMPTY_BYTE_ARRAY; } byte newbuf[] = new byte[remaining]; int pos =
   * 0; for (int i = 0; i < buffers.size(); i++) { byte[] buf = getBuffer(i);
   * int c = Math.min(buf.length, remaining); System.arraycopy(buf, 0, newbuf,
   * pos, c); pos += c; remaining -= c; if (remaining == 0) { break; } } return
   * newbuf; } }
   */
  public OutputStream getOutputStream() throws IOException {
    if (output == null) {
      output = new ByteArrayOutputStream();
    }
    return output;
  }

  public InputStream getInputStream() {
    return new ByteArrayInputStream();
  }
}
